/**
 * Runs the main loop of the game and handles all input / output
 * associated with runing and playing Chips Challenge 440. This
 * includes initiating the board (if necessary), making requests
 * for player movements, spawning windows and menus, and displaying the
 * game board and other information for playing the game.
 *
 * @author Ryan Hofler
 * @version 0.1
 */

package chips440.main;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.awt.image.BufferStrategy;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Popup;
import java.io.FileNotFoundException;
import java.applet.Applet;
import java.applet.AudioClip;
import java.net.URL;

/* these should be removed by the networked version */
import javax.imageio.ImageIO;
import java.io.FileInputStream;
/* end to be removed */

public class Chips440 extends Canvas implements KeyListener{
    private final int VIEW_LIMIT = 4;
    private final int VIEW_TILES = 9;
    private final int WINDOW_HEIGHT = 510;
    private final int WINDOW_WIDTH = 1200;
    private final int MP_OFF = 690;
    private final int O_TEXT_X = WINDOW_WIDTH / 4;
    private final int O_TEXT_Y = WINDOW_HEIGHT / 2;
    private final int IMAGE_SIZE = 50;
    private final int OFFSET = 15;
    private final int TEXT_X = 500;
    private final int TIME_Y = 75;
    private final int CHIPS_Y = 150;
    private final int BOOTS_Y = 300;
    private final int BOX_OFF = 40;
    private final int KEYS_Y = 325;
    private final int LINE_DIF = 25;
    private final int WL_TEXT = 40;
    private final int ORIGIN = 0;
    private final int IBOX_OFF = 12;
    private final int BOX_SPACE = 20;
    private final int ITEM_SPACE = 100;
    private final boolean MULTI_PLAYER = true;
    private final int P_ONE = 0;
    private final int P_TWO = 1;
    private final int MIN_PLAY = 1;
    private final int MAX_PLAY = 2;
    private final String SONG = "CHIP01.MID";
	private final String WIN = "WINLEVEL.WAV";

    private AudioClip song;
	private AudioClip victory;

    private int[] player_uid;
    private static boolean m_players;
    private static Chips440 chips;
    private Engine myEngine;

    private BufferStrategy strategy;
    private long time_left = 0;
    private int chips_left = 0;
    private int num_play;
    private Cell player1_pos;
    private Cell player2_pos;
    private static boolean playing = false;
    private BufferedImage player_image;
    private BufferedImage player2_image;

    // doesn't engine have a get state?
    private static Types.EngineState game_state = Types.EngineState.ALIVE;

    /**
     * the sole constructor for a new Chips440; this creates the window in
     * which the game will be played.
     */
    public Chips440() throws FileNotFoundException{
	this.m_players = !MULTI_PLAYER;
        make_window(new JFrame("Chips Challenge 440"), WINDOW_WIDTH,
		    WINDOW_HEIGHT, true);

        createBufferStrategy(2);

	this.song = ResourceCache.getSound(SONG);
	this.victory = ResourceCache.getSound(WIN);

	song.loop();

        strategy = getBufferStrategy();

	Graphics2D graphics = (Graphics2D) strategy.getDrawGraphics();
        graphics.setColor(Color.black);
        graphics.fillRect(ORIGIN, ORIGIN, WINDOW_WIDTH, WINDOW_HEIGHT);
	graphics.setColor(Color.white);
	graphics.drawString("rhofler and iev present:", OFFSET, OFFSET);
        graphics.setFont(new Font("SansSerif", Font.BOLD, WL_TEXT));
	graphics.drawString("Chips Challenge 440!", O_TEXT_X, O_TEXT_Y);
	strategy.show();
    }

    /**
     * initializes the Chips440 called chips and waits for a new game to
     * begin. If a new game is to begin, it calls new_game. This is the main
     * game loop.
     *
     * @param args command line arguments that are totally ignored
     */
    public static void main(String[] args){
        try{
	    chips = new Chips440();
	} catch (FileNotFoundException f){
	    System.out.println("uh oh");
	}
        while(true){
            while(!playing);
            chips.new_game(m_players);
	    while(game_state != Types.EngineState.ALIVE);
	    playing = true;
        }
    }

    /**
     * initiates a new game by calling initiate_game and starts the game by
     * calling playGame.
     */
    public void new_game(boolean mplayer){
	myEngine = new Engine();
	this.m_players = mplayer;
	if(!m_players)
	    num_play = MIN_PLAY;
	else
	    num_play = MAX_PLAY;
	this.player_uid = new int[MAX_PLAY];
	Cell uids = myEngine.init(mplayer);
	
	int one_id = uids.getX();
	int two_id = uids.getY();
	
	this.player_uid[P_ONE] = one_id;
	this.player_uid[P_TWO] = two_id;
	
	player_image = ResourceCache.getImage(one_id);
	player2_image = ResourceCache.getImage(two_id);
        chips.playGame();
    }

    /**
     * runs the main game loop While a game is in progress and the window is
     * visible, this loop checks for game state updates and draws the game
     * state by calling updateChips and paintChips.
     */
    public void playGame(){
        addKeyListener(this);
        requestFocus();
        while(isVisible() && playing){
            updateChips();
            paintChips();
            win_or_lose();
        }
        removeKeyListener(this);
    }

    /**
     * checks the game state to determine if the player has won or lost and
     * displays the appropriate message / allows continuation to the next
     * level or game ending.
     */
    public void win_or_lose(){
        if(game_state == Types.EngineState.ALIVE)
            return;
        Graphics2D graphics = (Graphics2D) strategy.getDrawGraphics();
        graphics.setColor(Color.red);
        graphics.setFont(new Font("SansSerif", Font.BOLD, WL_TEXT));
        if(game_state == Types.EngineState.WIN){
            graphics.drawString("Victory", TEXT_X, IMAGE_SIZE);
			this.victory.play();
	    strategy.show();
	    while(myEngine.getGameState() == Types.EngineState.WIN);
	}
        else{
            graphics.drawString("Game Over", TEXT_X, IMAGE_SIZE);
            strategy.show();
	    playing = false;
        }
    }

    /**
     * gets the current position of the player on the board, the time left,
     * and the number of chips left.
     */
    public void updateChips(){
        player1_pos = myEngine.getPlayerPosition(this.player_uid[P_ONE]);
	player2_pos = myEngine.getPlayerPosition(this.player_uid[P_TWO]);
        time_left = myEngine.getTimeLeft();
        chips_left = myEngine.getChipsLeft();
        game_state = myEngine.getGameState();
    }

    /**
     * draws the current visible secitons of the board in the window.  It also
     * displays the current time remaining and the number of chips remaining.
     */
    public void paintChips(){
        Graphics2D graphics = (Graphics2D) strategy.getDrawGraphics();
        graphics.setColor(Color.black);
        graphics.fillRect(ORIGIN, ORIGIN, WINDOW_WIDTH, WINDOW_HEIGHT);

        graphics.setColor(Color.white);
        graphics.drawString("Time Remaining:     " + time_left, TEXT_X, TIME_Y);
        graphics.drawString("Chips Remaining:    " + chips_left, TEXT_X, CHIPS_Y);

	// paint the display of player's items; this is really gross but
	// there's no choice
	for(int p_num = P_ONE; p_num < num_play; p_num++){
	    int p_uid = this.player_uid[p_num];
	    int x_pos = TEXT_X + BOX_OFF;
	    int player_off = ITEM_SPACE * p_num;
	    int y_boot = BOOTS_Y - IBOX_OFF + player_off;
	    int y_key = KEYS_Y - IBOX_OFF + player_off;

	    graphics.setColor(Color.white);
	    graphics.drawString("Player " + (p_num + MIN_PLAY), TEXT_X, BOOTS_Y
				+ player_off - LINE_DIF);
	    graphics.drawString("Boots", TEXT_X, BOOTS_Y + player_off);
	    graphics.drawString("Keys", TEXT_X, KEYS_Y + player_off);

	    if(myEngine.hasItem(p_uid, Types.Item.F_BOOT))
		graphics.setColor(Color.red);
	    else
		graphics.setColor(Color.gray);
	    graphics.fillRect(x_pos, y_boot, OFFSET, OFFSET);
	    if(myEngine.hasItem(p_uid, Types.Item.R_KEY))
		graphics.setColor(Color.red);
	    else
		graphics.setColor(Color.gray);
	    graphics.fillRect(x_pos, y_key, OFFSET, OFFSET);

	    x_pos += BOX_SPACE;

	    if(myEngine.hasItem(p_uid, Types.Item.W_BOOT))
		graphics.setColor(Color.blue);
	    else
		graphics.setColor(Color.gray);
	    graphics.fillRect(x_pos, y_boot, OFFSET, OFFSET);
	    if(myEngine.hasItem(p_uid, Types.Item.B_KEY))
		graphics.setColor(Color.blue);
	    else
		graphics.setColor(Color.gray);
	    graphics.fillRect(x_pos, y_key, OFFSET, OFFSET);

	    x_pos += BOX_SPACE;

	    if(myEngine.hasItem(p_uid, Types.Item.I_BOOT))
		graphics.setColor(Color.white);
	    else
		graphics.setColor(Color.gray);
	    graphics.fillRect(x_pos, y_boot, OFFSET, OFFSET);
	    if(myEngine.hasItem(p_uid, Types.Item.Y_KEY))
		graphics.setColor(Color.yellow);
	    else
		graphics.setColor(Color.gray);
	    graphics.fillRect(x_pos, y_key, OFFSET, OFFSET);

	    x_pos += BOX_SPACE;

	    if(myEngine.hasItem(p_uid, Types.Item.M_BOOT))
		graphics.setColor(Color.green);
	    else
		graphics.setColor(Color.gray);
	    graphics.fillRect(x_pos, y_boot, OFFSET, OFFSET);
	    if(myEngine.hasItem(p_uid, Types.Item.G_KEY))
		graphics.setColor(Color.green);
	    else
		graphics.setColor(Color.gray);
	    graphics.fillRect(x_pos, y_key, OFFSET, OFFSET);

	}

	int pos_xone = player1_pos.getX();
	int pos_yone = player1_pos.getY();
	int pos_ytwo = 0;
	int pos_xtwo = 0;
	if(m_players){
	    pos_xtwo = player2_pos.getX();
	    pos_ytwo = player2_pos.getY();
	}

        int img_y = OFFSET;
        for(int x = pos_xone - VIEW_LIMIT; x <= pos_xone + VIEW_LIMIT; x++){
	    int img_x = OFFSET;
            for(int y = pos_yone - VIEW_LIMIT; y <= pos_yone + VIEW_LIMIT; y++){
		graphics.drawImage(myEngine.getImage(x, y), img_x, img_y, this);
		if(m_players && (x == pos_xtwo && y == pos_ytwo))
                    graphics.drawImage(player2_image, img_x, img_y, this);
                if(x == pos_xone && y == pos_yone)
                    graphics.drawImage(player_image, img_x, img_y, this);
                img_x += IMAGE_SIZE;
            }
            img_y += IMAGE_SIZE;
        }

	if(m_players){
	    img_y = OFFSET;
	    for(int x = pos_xtwo - VIEW_LIMIT; x <= pos_xtwo + VIEW_LIMIT; x++){
		int img_x = MP_OFF;
		for(int y = pos_ytwo - VIEW_LIMIT; y <= pos_ytwo + VIEW_LIMIT; y++){
		    graphics.drawImage(myEngine.getImage(x, y), img_x, img_y, this);
		    if(x == pos_xone && y == pos_yone)
			graphics.drawImage(player_image, img_x, img_y, this);
		    if(x == pos_xtwo && y == pos_ytwo)
			graphics.drawImage(player2_image, img_x, img_y, this);
		    img_x += IMAGE_SIZE;
		}
		img_y += IMAGE_SIZE;
	    }

	}


        strategy.show();
    }

    /**
     * makes a window by taking a JFrame, width, height, and whether or not
     * closing the window ends the program and gives the JFrame the
     * appropriate characteristics.
     *
     * @param window jframe for the window
     * @param width width of the new window in pixels
     * @param height height of the new window in pixels
     * @param close should this window exit on close
     */
    public void make_window(JFrame window, int width, int height, boolean close){
        JPanel panel = (JPanel) window.getContentPane();
        setBounds(ORIGIN, ORIGIN, width, height);
        panel.setPreferredSize(new Dimension(width, height));
        panel.setLayout(null);
        panel.add(this);
        window.setBounds(ORIGIN, ORIGIN, width, height);

        if(close){
            menu(window);
            window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        }

        window.setResizable(false);
        requestFocus();
        window.pack();
        window.setVisible(true);
    }

    /**
     * creates the game menu for the game window.
     *
     * @param window jframe for the menu to be drawn on
     */
    public void menu(JFrame window){
        JMenuBar menu_bar;
        JMenu game;
        JMenuItem menu_item;

        menu_bar = new JMenuBar();
        game = new JMenu("Game");
        game.setMnemonic(KeyEvent.VK_A);
        menu_bar.add(game);

        menu_item = new JMenuItem("New Game");
        menu_item.setMnemonic(KeyEvent.VK_B);
        menu_item.setAccelerator(KeyStroke.getKeyStroke("control N"));
        menu_item.addActionListener(new NewGame());
        game.add(menu_item);

        menu_item = new JMenuItem("New Multiplayer Game");
	menu_item.setMnemonic(KeyEvent.VK_B);
	menu_item.setAccelerator(KeyStroke.getKeyStroke("control M"));
        menu_item.addActionListener(new MPNewGame());
        game.add(menu_item);

        menu_item = new JMenuItem("Rules");
        menu_item.setMnemonic(KeyEvent.VK_B);
        menu_item.setAccelerator(KeyStroke.getKeyStroke("control R"));
        menu_item.addActionListener(new Rules());
        game.add(menu_item);

        menu_item = new JMenuItem("About");
        menu_item.setMnemonic(KeyEvent.VK_B);
        menu_item.setAccelerator(KeyStroke.getKeyStroke("control A"));
        menu_item.addActionListener(new About());
        game.add(menu_item);

        game.addSeparator();

        menu_item = new JMenuItem("Quit");
        menu_item.setMnemonic(KeyEvent.VK_Q);
        menu_item.setAccelerator(KeyStroke.getKeyStroke("control Q"));
        menu_item.addActionListener(new Quit());
        game.add(menu_item);

        window.setJMenuBar(menu_bar);
    }

    /**
     * keyListener method that is executed upon keyboard input.  If the key
     * used is an arrow key, the board is informed of the attempted
     * directional move.
     *
     * @param event the KeyEvent passed in by the GUI system
     */
    public void keyPressed(KeyEvent event){
        int key = event.getKeyCode();
	
	switch(key){
	    // PLAYER TWO'S MOVEMENT
        case KeyEvent.VK_LEFT:
            myEngine.move(Types.Direct.LEFT, this.player_uid[P_TWO]);
	    break;
        case KeyEvent.VK_RIGHT:
            myEngine.move(Types.Direct.RIGHT, this.player_uid[P_TWO]);
	    break;
        case KeyEvent.VK_UP:
            myEngine.move(Types.Direct.UP, this.player_uid[P_TWO]);
	    break;
        case KeyEvent.VK_DOWN:
            myEngine.move(Types.Direct.DOWN, this.player_uid[P_TWO]);
	    break;
	    
	    // PLAYER ONE'S MOVEMENT
	case KeyEvent.VK_A:
            myEngine.move(Types.Direct.LEFT, this.player_uid[P_ONE]);
	    break;
        case KeyEvent.VK_D:
            myEngine.move(Types.Direct.RIGHT, this.player_uid[P_ONE]);
	    break;
        case KeyEvent.VK_W:
            myEngine.move(Types.Direct.UP, this.player_uid[P_ONE]);
	    break;
        case KeyEvent.VK_S:
            myEngine.move(Types.Direct.DOWN, this.player_uid[P_ONE]);
	    break;

	    // PLAYER ONE'S ITEM GIVING
	case KeyEvent.VK_1:
	    if(event.isShiftDown())
		myEngine.giveItem(Types.Item.F_BOOT, this.player_uid[P_ONE], this.player_uid[P_TWO]);
	    else
		myEngine.giveItem(Types.Item.R_KEY, this.player_uid[P_ONE], this.player_uid[P_TWO]);
	    break;
	case KeyEvent.VK_2:
	    if(event.isShiftDown())
		myEngine.giveItem(Types.Item.W_BOOT, this.player_uid[P_ONE], this.player_uid[P_TWO]);
	    else
		myEngine.giveItem(Types.Item.B_KEY, this.player_uid[P_ONE], this.player_uid[P_TWO]);
	    break;
	case KeyEvent.VK_3:
	    if(event.isShiftDown())
		myEngine.giveItem(Types.Item.I_BOOT, this.player_uid[P_ONE], this.player_uid[P_TWO]);
	    else
		myEngine.giveItem(Types.Item.Y_KEY, this.player_uid[P_ONE], this.player_uid[P_TWO]);
	    break;
	case KeyEvent.VK_4:
	    if(event.isShiftDown())
		myEngine.giveItem(Types.Item.M_BOOT, this.player_uid[P_ONE], this.player_uid[P_TWO]);
	    else
		myEngine.giveItem(Types.Item.G_KEY, this.player_uid[P_ONE], this.player_uid[P_TWO]);
	    break;
	    
	    // PLAYER TWO'S ITEM GIVING
	case KeyEvent.VK_7:
	    if(event.isShiftDown())
		myEngine.giveItem(Types.Item.F_BOOT, this.player_uid[P_TWO], this.player_uid[P_ONE]);
	    else
		myEngine.giveItem(Types.Item.R_KEY, this.player_uid[P_TWO], this.player_uid[P_ONE]);
	    break;
	case KeyEvent.VK_8:
	    if(event.isShiftDown())
		myEngine.giveItem(Types.Item.W_BOOT, this.player_uid[P_TWO], this.player_uid[P_ONE]);
	    else
		myEngine.giveItem(Types.Item.B_KEY, this.player_uid[P_TWO], this.player_uid[P_ONE]);
	    break;
	case KeyEvent.VK_9:
	    if(event.isShiftDown())
		myEngine.giveItem(Types.Item.I_BOOT, this.player_uid[P_TWO], this.player_uid[P_ONE]);
	    else
		myEngine.giveItem(Types.Item.Y_KEY, this.player_uid[P_TWO], this.player_uid[P_ONE]);
	    break;
	case KeyEvent.VK_0:
	    if(event.isShiftDown())
		myEngine.giveItem(Types.Item.M_BOOT, this.player_uid[P_TWO], this.player_uid[P_ONE]);
	    else
		myEngine.giveItem(Types.Item.G_KEY, this.player_uid[P_TWO], this.player_uid[P_ONE]);
	    break;
	}
    }

    /**
     * a keyListener method; this method is not implemented currently.
     */
    public void keyReleased(KeyEvent event){}

    /**
     * a keyListener method; this method is not implemented currently.
     */
    public void keyTyped(KeyEvent e){}

    /**
     * the ActionListener for the Quit menu button Quits the game.
     */
    private class Quit implements ActionListener{
        public void actionPerformed(ActionEvent event){
	    song.stop();
            System.exit(0);
        }
    }

    private class Rules implements ActionListener{
        public void actionPerformed(ActionEvent event){
            JOptionPane.showMessageDialog(null,
					  "Collect all the chips and get to the exit before time"+
					  "\nruns out. But be careful: some tiles are deadly!"+
					  "\nPick up keys to open doors and boots to walk safely"+
					  "\non dangerous surfaces!"+
					  "\nAlso remember that keys can only be used ONCE!"+
					  "\n\nMovement:"+
					  "\nPlayer one moves with W,A,S,D, Player two uses the"+
					  "\narrow Keys."+
					  "\n\nGiving Items:"+
					  "\nPlayer one: keys: 1,2,3,4, Boots: shift+1,2,3,4"+
					  "\nPlayer two: keys: 7,8,9,0, Boots: shift+7,8,9,0",
					  "Chips Challenge 440 Rules",
					  JOptionPane.PLAIN_MESSAGE);
        }
    }

    private class About implements ActionListener{
        public void actionPerformed(ActionEvent event){
            JOptionPane.showMessageDialog
		(null,
		 "Chips Challenge 440:"+
		 "\nWritten by Ryan Hofler and Ian Voysey"+
		 "\nfor 15-440 Fall 2008.",
		 "About Chips Challenge 440",
		 JOptionPane.PLAIN_MESSAGE);
        }
    }

    /**
     * the ActionListener for the New Game menu button Restarts the game.
     *
     * @author Ryan Hofler
     * @version 0.1
     */
    private class NewGame implements ActionListener{

	/**
	 * uses an ActionEvent to implement pausing the game
	 */
        public void actionPerformed(ActionEvent event){
	    m_players = !MULTI_PLAYER;
            requestFocus();
            if(!playing)
                playing = true;
            else
                playing = false;
            game_state = Types.EngineState.ALIVE;
        }
    }

    /**
     * the ActionListener for the New Game menu button Restarts the multiplayer game.
     *
     * @author Ryan Hofler
     * @version 0.1
     */
    private class MPNewGame implements ActionListener{

	/**
	 * uses an ActionEvent to implement pausing the game
	 */
        public void actionPerformed(ActionEvent event){
	    m_players = MULTI_PLAYER;
            requestFocus();
            if(!playing)
                playing = true;
            else
                playing = false;
            game_state = Types.EngineState.ALIVE;
        }
    }

}
